package org.codefilarete.stalactite.sql.result;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.tool.function.Hanger.Holder;

/**
 * A library of {@link Accumulator}
 * 
 * @author Guillaume Mary
 */
public final class Accumulators {
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new {@link Collection} (created by given factory).
	 * 
	 * @param <T> type of elements
	 * @param <C> resulting {@link Collection} type
	 * @param collectionFactory a {@link Supplier} returning a new {@link Collection} of appropriate type
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Collection}, in encounter order
	 */
	public static <T, C extends Collection<T>> Accumulator<T, C, C> toCollection(Supplier<C> collectionFactory) {
		return new CollectionSupport<>(collectionFactory);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new {@link List}.
	 * 
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@code List}, in encounter order
	 * @see #toUnmodifiableList() 
	 */
	public static <T> Accumulator<T, ? extends List<T>, List<T>> toList() {
		return new CollectionSupport<>(ArrayList::new);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new unmodifiable {@link List}.
	 * 
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@link List}, in encounter order
	 */
	public static <T> Accumulator<T, ? extends List<T>, List<T>> toUnmodifiableList() {
		return Accumulators.<T>toList().andThen(Collections::unmodifiableList);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new {@link Set}.
	 * 
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T> Accumulator<T, ? extends Set<T>, Set<T>> toSet() {
		return new CollectionSupport<>(HashSet::new);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new unmodifiable {@link Set}.
	 * 
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T> Accumulator<T, ? extends Set<T>, Set<T>> toUnmodifiableSet() {
		return Accumulators.<T>toSet().andThen(Collections::unmodifiableSet);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new {@link Set} and keep encounter order.
	 * Expected use is for query which is already sorted : it avoids to lose sorting order, as a difference with {@link #toSet()}.
	 *
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T> Accumulator<T, Set<T>, Set<T>> toKeepingOrderSet() {
		return new CollectionSupport<>(LinkedHashSet::new);
	}
	
	/**
	 * Creates an {@link Accumulator} that adds input elements into a new {@link NavigableSet}.
	 *
	 * @param comparator the {@link Comparator} used to compare input elements
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T> Accumulator<T, NavigableSet<T>, NavigableSet<T>> toSortedSet(Comparator<T> comparator) {
		return new CollectionSupport<>(() -> new TreeSet<>(comparator));
	}
	
	/**
	 * Creates an {@link Accumulator} that puts input elements into a new {@link Map} as value with key given by
	 * {@code keyMapper}.
	 * If key is encountered several times, only first element is kept in final result.
	 * 
	 * @param <T> type of input elements
	 * @param <K> type of {@link Map} key
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T, K> Accumulator<T, ?, Map<K, T>> groupingBy(Function<? super T, ? extends K> keyMapper) {
		return groupingBy(keyMapper, HashMap::new, getFirst());
	}
	
	/**
	 * Creates an {@link Accumulator} that puts input elements into a new {@link Map} with key given by
	 * {@code keyMapper} and value given by {@code downstream}.
	 * If key is encountered several times, only first element is kept in final result.
	 * 
	 * @param <T> type of input elements
	 * @param <K> type of {@link Map} key
	 * @param <V> type of {@link Map} value
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T, K, V> Accumulator<T, ?, Map<K, V>> groupingBy(
			Function<? super T, ? extends K> keyMapper,
			Accumulator<? super T, ?, V> downstream) {
		return groupingBy(keyMapper, HashMap::new, downstream);
	}
	
	/**
	 * Creates an {@link Accumulator} that puts input elements into a new {@link Map} with key given by
	 * {@code keyMapper} and value given by {@code downstream}.
	 * {@link Map} instance creation is controlled by {@code mapFactory}.
	 * 
	 * @param <T> type of input elements
	 * @param <K> type of {@link Map} key
	 * @param <V> type of {@link Map} value
	 * @param <M> type of resulting {@link Map}
	 * @param <S> type of {@code downStream} seed
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T, K, V, S, M extends Map<K, V>> Accumulator<T, ?, M> groupingBy(
			Function<? super T, ? extends K> keyMapper,
			Supplier<M> mapFactory,
			Accumulator<? super T, S, V> downstream) {
		// Algorithm : resulting Map is built by mapFactory and aggregation is made by downstream one, which puts object
		// of wrong type in Map, which is doable thanks to Map plasticity, finally all values are replaced by downstream
		// finisher with appropriate type
		Supplier<S> downstreamSupplier = downstream.supplier();
		BiConsumer<S, ? super T> downstreamAggregator = downstream.aggregator();
		BiConsumer<Map<K, S>, T> accumulator = (m, t) -> {
			K key = keyMapper.apply(t);
			S container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
			downstreamAggregator.accept(container, t);
		};
		// Downstream finisher will replace every value by its own type in resulting map
		Function<S, S> downstreamFinisher = (Function<S, S>) downstream.finisher();
		Function<Map<K, S>, M> finisher = resultingMap -> {
			resultingMap.replaceAll((k, v) -> downstreamFinisher.apply(v));
			return (M) resultingMap;
		};
		return new AccumulatorSupport<>((Supplier<Map<K, S>>) mapFactory, accumulator, finisher);
	}
	
	/**
	 * Creates an {@link Accumulator} that extracts a property from input elements and aggregates it into another {@link Accumulator}.
	 *
	 * @param <T> type of input elements
	 * @param <K> type of extracting property
	 * @param <S> type of the seed of the secondary {@link Accumulator}
	 * @param <R> type of the result
	 * @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
	 */
	public static <T, K, S, R> Accumulator<T, ?, R> mapping(
			Function<? super T, ? extends K> mapper,
			Accumulator<? super K, S, R> downstream) {
		BiConsumer<S, ? super K> downstreamAccumulator = downstream.aggregator();
		return new AccumulatorSupport<>(downstream.supplier(),
				(r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)),
				downstream.finisher());
	}
	
	/**
	 * Creates an {@link Accumulator} that returns first non-null input element (will return null if no non-null
	 * elements were found).
	 * <strong>It will return first element / bean / entity, not row. Meaning that whole {@link java.sql.ResultSet} will
	 * be consumed to build the bean.</strong>
	 *
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which returns first non-null input element
	 */
	public static <T> Accumulator<T, ?, T> getFirst() {
		return new AccumulatorSupport<T, Holder<T>, T>(Holder::new, (holder, t) -> {
			if (holder.get() == null) {
				holder.set(t);
			}
		}, Holder::get);
	}
	
	/**
	 * Creates an {@link Accumulator} that returns first non-null input element which is expected to be the only one :
	 * will throw an exception if another non-null object is found
	 *
	 * @param <T> type of input elements
	 * @return an {@link Accumulator} which returns last input element
	 */
	public static <T> Accumulator<T, ?, T> getFirstUnique() {
		return new AccumulatorSupport<T, Holder<T>, T>(Holder::new, (holder, t) -> {
			if (holder.get() != null) {
				throw new NonUniqueObjectException("Object was expected to be a lonely one but another object is present");
			}
			holder.set(t);
		}, Holder::get);
	}
	
	static class CollectionSupport<T, C extends Collection<T>> extends AccumulatorSupport<T, C, C> {
		
		CollectionSupport(Supplier<C> supplier) {
			super(supplier, Collection::add);
		}
	}
	
	/**
	 * Generic {@link Accumulator} implementation class.
	 *
	 * @param <T> the type of elements to be collected
	 * @param <S> the type of the seed that collects elements
	 * @param <R> the type of the result
	 */
	static class AccumulatorSupport<T, S, R> implements Accumulator<T, S, R> {
		private final Supplier<S> supplier;
		private final BiConsumer<S, T> accumulator;
		private final Function<S, R> finisher;
		
		AccumulatorSupport(Supplier<S> supplier,
						   BiConsumer<S, T> accumulator,
						   Function<S, R> finisher) {
			this.supplier = supplier;
			this.accumulator = accumulator;
			this.finisher = finisher;
		}
		
		AccumulatorSupport(Supplier<S> supplier,
						   BiConsumer<S, T> accumulator) {
			this(supplier, accumulator, s -> (R) s);
		}
		
		@Override
		public Supplier<S> supplier() {
			return this.supplier;
		}
		
		@Override
		public BiConsumer<S, T> aggregator() {
			return this.accumulator;
		}
		
		@Override
		public Function<S, R> finisher() {
			return this.finisher;
		}
	}
	
	public static class NonUniqueObjectException extends RuntimeException {
		
		public NonUniqueObjectException(String message) {
			super(message);
		}
	}
	
	/** Not-exposed constructor to prevent from instantiation to apply tool class design */
	private Accumulators() {
		// voluntarily empty
	}
}